/* Copyright (c) 2015,2016 Lucky Byte, Inc.
 */
package com.lucky_byte.pay.jar.p091;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.StandardSocketOptions;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.bouncycastle.util.encoders.Hex;

import com.google.gson.JsonObject;
import com.lucky_byte.pay.jar.ByteUtils;
import com.lucky_byte.pay.jar.JdbcRecord;
import com.lucky_byte.pay.jar.JdbcTable;

/**
 * POS 009.1 接出渠道
 */
public class Fowder091
{
	private static final Logger logger = LogManager.getLogger();

	private JdbcRecord ochnl;
	private SocketChannel socket;
	private Selector selector;
	private JsonObject netinfo;

	public Fowder091() {
		this.netinfo = new JsonObject();
	}

	/**
	 * 返回网络通讯信息，用于记录流水
	 */
	public JsonObject getNetinfo() {
		return this.netinfo;
	}

	/**
	 * 转发接入渠道接收的报文
	 */
	public Packet091 forward(Packet091 packet, boolean indirect) {
		// 对于透传模式，直接发送报文的原始数据
		if (!indirect) {
			logger.debug("POS 009.1 透传模式下直接转发原始终端报文.");
			byte[] bytes = packet.inBytes();
			if (bytes == null) {
				logger.error("POS 009.1 透传模式下转发的报文非原始报文，拒绝转发.");
				return this.respkt(packet, "57");
			}
			return this.forward(packet, bytes);
		}
		// 对于间连模式，需要重组报文
		byte[] bytes = packet.packBytes();
		if (bytes == null) {
			logger.error("POS 009.1 非透传模式下转发时重组报文失败.");
			return null;
		}
		return this.forward(packet, bytes);
	}

	/**
	 * 转发报文
	 */
	private Packet091 forward(Packet091 packet, byte[] bytes) {
		if (bytes.length < 21) {
			logger.error("POS 009.1 转发报文长度[{}]无效.", bytes.length);
			return this.respkt(packet, "96");
		}
		// 查询 POS 009.1 接出渠道配置
		JdbcTable table = JdbcTable.listAll_NE("pay_o_091");
		if (table == null || table.getRecordCount() == 0) {
			logger.error("未查询到 POS 009.1 接出渠道配置.");
			return this.respkt(packet, "96");
		}
		ochnl = table.getRecord(0);
		if (ochnl.getBoolean("disabled")) {
			logger.error("POS 009.1 接出渠道已被禁用，不能转发报文.");
			return this.respkt(packet, "57");
		}
		// 置换请求报文的 TPDU 为接出渠道配置的 TPDU
		byte[] reqt_tpdu = null;
		String tpdu = ochnl.getString("tpdu");
		if (tpdu != null) {
			tpdu = tpdu.trim();
			if (tpdu.length() != 10) {
				logger.error("POS 009.1 接出渠道配置的TPDU[{}]长度错误.", tpdu);
				return this.respkt(packet, "96");
			}
			try {
				byte[] tpdu_bytes = Hex.decode(tpdu);
				reqt_tpdu = new byte[tpdu_bytes.length];
				for (int i = 0; i < tpdu_bytes.length; i++) {
					reqt_tpdu[i] = bytes[i];
					bytes[i] = tpdu_bytes[i];
				}
			} catch (Exception e) {
				logger.error("POS 009.1 接出渠道配置的TPDU[{}]无效.", tpdu);
				return this.respkt(packet, "96");
			}
		}
		// 开始连接服务器并收发数据
		if (!this.connect()) {
			return this.respkt(packet, "92");
		}
		this.setNetinfo();

		int ret = this.send(bytes);
		if (ret < 0) {
			this.close();
			return this.respkt(packet, "92");
		}
		byte[] recv_bytes = this.recv();
		this.close();

		// 如果发送成功后响应失败，则可能需要冲正，因此返回 null
		if (recv_bytes == null) {
			return null;
		}
		// 置换响应报文的 TPDU 为转发前的 TPDU
		if (reqt_tpdu != null) {
			for (int i = 0; i < reqt_tpdu.length; i++) {
				recv_bytes[i] = reqt_tpdu[i];
			}
		}
		Packet091 respkt = new Packet091();
		if (!respkt.readBytes(recv_bytes, -1)) {
			logger.error("解析 POS 009.1 接出渠道响应报文错误.");
			return null;
		}
		return respkt;
	}

	/**
	 * 连接服务器
	 */
	protected boolean connect(String remote_addr, int remote_port) {
		logger.info("开始连接 POS 009.1 接出渠道[{}][{}].",
				remote_addr, remote_port);
		try {
			InetSocketAddress inet_addr =
					new InetSocketAddress(remote_addr, remote_port);
			socket = SocketChannel.open();
			socket.setOption(StandardSocketOptions.SO_KEEPALIVE, false);
			socket.socket().connect(inet_addr, 10000);

			selector = Selector.open();
			socket.configureBlocking(false);
			socket.register(selector, SelectionKey.OP_READ);

			logger.info("连接 POS 009.1 接出渠道[{}][{}]成功.",
					remote_addr, remote_port);
			return true;
		} catch (Exception e) {
			logger.error("连接 POS 009.1 接出渠道[{}][{}] 失败[{}][{}].", 
					remote_addr, remote_port, e.getMessage(),
					e.getClass().getSimpleName());
			return false;
		}
	}

	/**
	 * 连接服务器
	 */
	private boolean connect() {
		String remote_addr = ochnl.getString("remote_addr");
		int remote_port = ochnl.getInteger("remote_port");
		if (remote_addr != null && remote_port > 0) {
			if (connect(remote_addr, remote_port)) {
				return true;
			}
		} else {
			logger.error("POS 009.1 接出渠道的地址或端口没有正确配置，请检查.");
		}
		logger.info("连接 POS 009.1 接出渠道失败，尝试连接备用服务器...");

		remote_addr = ochnl.getString("remote_addr2");
		remote_port = ochnl.getInteger("remote_port2");
		if (remote_addr != null && remote_port > 0) {
			return connect(remote_addr, remote_port);
		}
		logger.error("POS 009.1 接出渠道的备用地址或端口没有正确配置，请检查.");
		return false;
	}

	/**
	 * 关闭连接
	 */
	public void close() {
		try {
			if (socket != null) {
				socket.close();
			}
			if (selector != null) {
				selector.close();
			}
		} catch (IOException e) {
			logger.catching(e);
		}
	}

	/**
	 * 收集网络信息
	 */
	private void setNetinfo() {
		try {
			InetSocketAddress addr =
					(InetSocketAddress) this.socket.getLocalAddress();
			this.netinfo.addProperty("c_addr", addr.getHostString());
			this.netinfo.addProperty("c_port", addr.getPort());

			addr = (InetSocketAddress) this.socket.getRemoteAddress();
			this.netinfo.addProperty("s_addr", addr.getHostString());
			this.netinfo.addProperty("s_port", addr.getPort());
		} catch (IOException e) {
			logger.error("转发时获取通讯信息错误[{}][{}]", e.getMessage(),
					e.getClass().getSimpleName());
		}
	}

	/**
	 * 发送数据
	 */
	public int send(byte[] bytes) {
		try {
			ByteBuffer byte_buff = ByteBuffer.allocate(bytes.length + 2);
			byte_buff.order(ByteOrder.BIG_ENDIAN);
			byte_buff.putShort((short) bytes.length);
			byte_buff.put(bytes);
			byte_buff.position(0);

			int send_size = 0;
			while (byte_buff.hasRemaining()) {
				send_size += socket.write(byte_buff);
			}
			ByteUtils.log("转发报文长度[" + bytes.length + "]，数据：", bytes);
			return send_size;
		} catch (IOException e) {
			logger.error("发送数据到 POS 009.1 接出渠道失败[{}][{}]",
					e.getMessage(), e.getClass().getSimpleName());
			return -1;
		}
	}

	/**
	 * 接收指定字节的数据，直到接收完毕或者出错
	 *
	 * @param size 欲接收的数据大小
	 * @return 接收到的数据，或 null
	 */
	protected ByteBuffer recv(int size) {
		try {
			int timeout = ochnl.getInteger("timeout") * 1000;
			ByteBuffer recv_buff = ByteBuffer.allocate(size);
			recv_buff.order(ByteOrder.BIG_ENDIAN);
			while (recv_buff.hasRemaining()) {
				int selected = selector.select(timeout);
				if (selected == 0) {
					logger.error("POS 009.1 接出渠道接收响应报文超时[{}秒].",
							timeout / 1000);
					return null;
				}
				Set<SelectionKey> key_set = selector.selectedKeys();
				Iterator<SelectionKey> iter = key_set.iterator();
				while (iter.hasNext()) {
					SelectionKey selected_key = iter.next();
					if (selected_key.isReadable()) {
						int nread = socket.read(recv_buff);
						if (nread == -1) {
							logger.error("POS 009.1 接出渠道关闭了链接，读取数据失败.");
							return null;
						}
					}
					iter.remove();
				}
			}
			String text = String.format("接收响应报文长度[%d], 数据:", size);
			ByteUtils.log(text, recv_buff.array());
			return recv_buff;
		} catch (IOException e) {
			logger.error("POS 009.1 接出渠道接收服务端的响应失败[{}][{}]",
					e.getMessage(), e.getClass().getSimpleName());
			return null;
		}
	}

	/**
	 * 接收报文数据
	 */
	public byte[] recv() {
		ByteBuffer size_buff = this.recv(2);
		if (size_buff == null) {
			logger.error("POS 009.1 接出渠道接收响应报文长度出错.");
			return null;
		}
		int size = size_buff.getShort(0);
		if (size < 0) {
			return null;
		}
		// 如果接收到心跳报文，则继续读，服务端可能发送心跳报文
		if (size == 0) {
			return this.recv();
		}
		ByteBuffer data_buff = this.recv(size);
		if (data_buff == null) {
			logger.error("POS 009.1 接出渠道读响应报文出错.");
			return null;
		}
		return data_buff.array();
	}

	/**
	 * 构造一个响应报文
	 */
	private Packet091 respkt(Packet091 packet, String code) {
		Packet091 respkt = (Packet091) packet.clone();
		String msgtype = packet.get("1");
		if (msgtype != null) {
			int msg_type = Integer.parseInt(msgtype) + 10;
			respkt.set("1", Integer.toString(msg_type));
		}
		respkt.set("39", code);
		respkt.remove("64");
		return respkt;
	}

}
